from __future__ import annotations

import asyncio
import calendar
from datetime import date, datetime, timedelta, timezone
from typing import Iterable

import structlog
from fastapi import FastAPI
from sqlalchemy import select
from sqlalchemy.orm import selectinload

from app.core.redis_client import get_redis
from app.db.session import async_session_factory
from app.models.anniversary import Anniversary
from app.services.lunar_utils import parse_lunar_str, next_solar_from_lunar
from app.services.notification_manager import notification_manager

logger = structlog.get_logger('love_notes.anniversary_scheduler')

CHECK_INTERVAL_SECONDS = 60 * 60  # hourly
CACHE_TTL_SECONDS = 3 * 24 * 60 * 60


def _normalize_day(year: int, month: int, day: int) -> date:
  last_day = calendar.monthrange(year, month)[1]
  return date(year, month, min(day, last_day))


def _next_occurrence(base: date, recurrence: str, reference: date) -> date | None:
  if recurrence == 'once':
    return base if base >= reference else None

  if recurrence == 'monthly':
    year = reference.year
    month = reference.month
    candidate = _normalize_day(year, month, base.day)
    if candidate < reference:
      if month == 12:
        year += 1
        month = 1
      else:
        month += 1
      candidate = _normalize_day(year, month, base.day)
    return candidate

  # default yearly/custom
  year = reference.year
  candidate = _normalize_day(year, base.month, base.day)
  if candidate < reference:
    candidate = _normalize_day(year + 1, base.month, base.day)
  return candidate


async def _scan_once(today: date) -> None:
  async with async_session_factory() as session:
    redis = get_redis()

    stmt = (
      select(Anniversary)
      .options(selectinload(Anniversary.user))
      .where(Anniversary.reminder_offset_days >= 0)
    )
    result = await session.scalars(stmt)
    anniversaries = result.all()

    for ann in anniversaries:
      # 计算 occurrence：支持农历字符串
      occurrence: date | None
      if ann.is_lunar and ann.lunar_date_str:
        patt = parse_lunar_str(ann.lunar_date_str)
        occurrence = next_solar_from_lunar(patt, today) if patt else None
      else:
        occurrence = _next_occurrence(ann.date, ann.recurrence, today)
      if occurrence is None:
        continue
      reminder_offset = ann.reminder_offset_days or 0
      reminder_day = occurrence - timedelta(days=reminder_offset)
      if reminder_day != today:
        continue

      cache_key = None
      if redis:
        cache_key = f'anniversary:reminder:{ann.id}:{occurrence.isoformat()}'
        exists = await redis.exists(cache_key)
        if exists:
          continue

      recipients: set[str] = set()
      if ann.user and ann.user.email:
        recipients.add(ann.user.email)
      # load partner email lazily to avoid import cycle
      if ann.user:
        from app.services.relationship_utils import get_partner_email

        partner_email = await get_partner_email(session, ann.user)
        if partner_email:
          recipients.add(partner_email)

      if not recipients:
        logger.info('Skip anniversary reminder without recipients', anniversary_id=ann.id)
        continue

      await notification_manager.notify_anniversary_reminder(
        recipient_emails=recipients,
        anniversary_name=ann.name,
        occurrence_date=occurrence.isoformat(),
        creator_name=ann.user.username if ann.user else '伴侣',
      )

      if cache_key and redis:
        await redis.set(cache_key, '1', ex=CACHE_TTL_SECONDS)


async def _scheduler_loop(app: FastAPI) -> None:
  logger.info('Anniversary scheduler started')
  try:
    while True:
      today = datetime.now(timezone.utc).date()
      try:
        await _scan_once(today)
      except Exception as exc:  # pragma: no cover - defensive logging
        logger.exception('Anniversary reminder scan failed', exc_info=exc)
      await asyncio.sleep(CHECK_INTERVAL_SECONDS)
  except asyncio.CancelledError:  # pragma: no cover
    logger.info('Anniversary scheduler cancelled')
    raise


async def start_anniversary_scheduler(app: FastAPI) -> None:
  if getattr(app.state, 'anniversary_task', None):
    return
  app.state.anniversary_task = asyncio.create_task(_scheduler_loop(app))


async def stop_anniversary_scheduler(app: FastAPI) -> None:
  task = getattr(app.state, 'anniversary_task', None)
  if task:
    task.cancel()
    try:
      await task
    except asyncio.CancelledError:
      pass
    app.state.anniversary_task = None
